package cn.blnp.net.flowable.task.user;

import cn.hutool.core.date.DateUtil;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.filter.SimplePropertyPreFilter;
import lombok.extern.slf4j.Slf4j;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.repository.ProcessDefinition;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.identitylink.api.IdentityLink;
import org.flowable.task.api.Task;
import org.flowable.task.api.TaskQuery;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <h3>用户任务演示案例</h3>
 *
 * @author <a href="mailto:blnp.yibin@qq.com">lyb</a>
 * @version 1.0
 * @since 2025/5/12 11:56
 */
@Slf4j
@SpringBootTest
public class UserTaskTest {

    @Resource
    private RepositoryService repositoryService;
    @Resource
    private RuntimeService runtimeService;
    @Resource
    private TaskService taskService;

    /**
     *  <p>
     *      用户任务需就是要人工参与处理。当流程流转到用户任务时，工作流引擎会给指定的用户（办理人或候选人）或一组用户（候选组）创建待处理的任务项，等待用户进行处理。
     *   <b>这里额外强调个知识点</b>，用户任务可以添加描述，实际上所有BPMN 2.0元素都可以添加描述。描述使用documentation元素定义，将其嵌入用户任务定义即可为用户任务
     *   添加描述，代码如下：
     *   <pre>
     *       {@code
     *          <userTask id="userTask1" name="用户任务">
     *              <documentation>
     *                  这是一个用户任务节点。
     *              </documentation>
     *          </userTask>
     *       }
     *   </pre>
     *  </p>
     *  <p>
     *      每个用户任务都可以设置一个过期时间。Flowable为用户任务提供了一个扩展属性dueDate，用于在用户任务定义中添加一个表达式，从而在用户任务创建时为其设置过期时间，代码如下：</br/>
     *      <pre>
     *          {@code
     *              <userTask id="userTask1" name="用户任务" flowable:dueDate="${dueDateVariable}"/>
     *          }
     *      </pre>
     *  </p>
     *  <p>
     *      <b style="color:red;">特别注意：</b>用户任务的过期时间除了可以在任务定义中配置，还可以通过TaskService提供的API进行修改，或在TaskListener中通过传入的DelegateTask参数进行修改。
     *   注意，dueDate属性只是标识该用户任务何时过期，但该用户<u>任务过期后不会自动完成</u>。在Flowable中，用户任务过期时间存储在ACT_RU_TASK表的DUE_DATE_字段中，Flowable提供了基于过期时间进行查询的API。
     *  </p>
     *  <h2>案例演示：</h2>
     *  <hr/>
     *  <p>
     *      流程发起后流转到“请假申请”用户任务，请假申请提交后流转到“经理审批”用户任务，该审批完成后流程结束。<br/>
     *      <b>流程定义文件：</b>{@link flow/userTaskProcess.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://cdn4.winhlb.com/2025/05/12/68218e2fb6246.png" />
     *  </p>
     **/
    @Test
    public void testTask() {
        Deployment deploy = repositoryService.createDeployment()
                .name("简单用户任务案例")
                .addClasspathResource("flow/userTaskProcess.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId());
        TaskQuery taskQuery = taskService.createTaskQuery()
                .processInstanceId(procInst.getId());

        //查询第一个任务
        Task task1 = taskQuery.singleResult();
        log.info("第一个任务taskId：{}，taskName为：{}", task1.getId(), task1.getName());
        log.info("用户任务描述信息为：{}", task1.getDescription());
        log.info("用户任务创建时间为：{}", DateUtil.formatDateTime(task1.getCreateTime()));
        //设置流程变量
        Map<String, Object> varMap = MapUtil.builder(new HashMap<String, Object>())
                .put("dayNum", 3)
                .put("applyReason", "休探亲假。")
                .build();
        //办理第一个任务
        taskService.complete(task1.getId(), varMap);
        //查询第二个任务
        Task task2 = taskQuery.singleResult();
        log.info("第二个任务taskId：{}，taskName为：{}", task2.getId(), task2.getName());
        log.info("用户任务描述信息为：{}", task2.getDescription());
        log.info("用户任务创建时间为：{}，过期时间为：{}", DateUtil.formatDateTime(task2.getCreateTime()),
                DateUtil.formatDateTime(task2.getDueDate()));

        /**
         *  演示结果：
         *  2025-05-12 14:07:47.705  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 部署流程定义：6d30a66d-2ef7-11f0-9d69-50ebf6ec8f6d
         * 2025-05-12 14:07:47.708  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 流程定义编号：userTaskProcess:1:6d77e8a0-2ef7-11f0-9d69-50ebf6ec8f6d
         * 2025-05-12 14:07:47.791  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 第一个任务taskId：6d7ffef6-2ef7-11f0-9d69-50ebf6ec8f6d，taskName为：请假申请
         * 2025-05-12 14:07:47.791  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 用户任务描述信息为：这是员工请假申请环节。
         * 2025-05-12 14:07:47.857  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 用户任务创建时间为：2025-05-12 14:07:47
         * 2025-05-12 14:07:47.994  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 第二个任务taskId：6da1dedb-2ef7-11f0-9d69-50ebf6ec8f6d，taskName为：经理审批
         * 2025-05-12 14:07:47.995  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 用户任务描述信息为：这是经理审批环节。
         * 2025-05-12 14:07:47.995  INFO 101632 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 用户任务创建时间为：2025-05-12 14:07:47，过期时间为：2025-05-12 16:07:47
         **/
    }

    /**
     *  <h2>用户任务分配</h2>
     *  <hr/>
     *  <p>
     *      用户任务可以直接分配给一个用户，这个任务只能出现在该用户的个人任务列表中，而不会出现在其他人的任务列表中。查看和办理这个任务的用户称为办理人。Flowable将用户任务分配给办理人的方式主要有以下两种：
     *      <ul>
     *          <li>通过humanPerformer元素定义；</li>
     *          <li>通过assignee属性定义。</li>
     *      </ul>
     *  </p>
     *  <h3>通过humanPerformer元素定义</h3>
     *  <p>
     *      这是BPMN 2.0规定的标准方式。humanPerformer是UserTask的子元素，采用这种方式，需要为humanPerformer定义一个resourceAssignmentExpression来实际定义办理人，目前Flowable支持通过formalExpression指派：
     *  <pre>
     *      {@code
     *          <userTask id='userTask1' name='分配给办理人的用户任务' >
     *              <humanPerformer>
     *                  <resourceAssignmentExpression>
     *                      <formalExpression>baby</formalExpression>
     *                  </resourceAssignmentExpression>
     *              </humanPerformer>
     *          </userTask>
     *      }
     *  </pre>
     *      或者方式二：
     *   <pre>
     *       {@code
     *          <userTask id="userTask2" name="分配给办理人的用户任务">
     *              <humanPerformer>
     *                  <resourceAssignmentExpression>
     *                      <formalExpression>${baby}</formalExpression>
     *                  </resourceAssignmentExpression>
     *              </humanPerformer>
     *          </userTask>
     *       }
     *   </pre>
     *  </p>
     *  <h3>通过assignee属性定义</h3>
     *  <p>
     *      <ol>
     *          <li>
     *              流程设计时直接指定分配：<br/>
     *              {@code
     *                  <userTask id="userTask3" name="分配给办理人的用户任务" flowable:assignee="biuBiu" />
     *              }
     *          </li>
     *          <li>
     *              通过表达式配置，代码如下：<br/>
     *              {@code
     *                  <userTask id="userTask4" name="分配给办理人的用户任务" flowable:assignee="${userName}" />
     *              }
     *          </li>
     *          <li>
     *              通过taskService服务提供的API重新指定办理人，示例代码如下：<br/>
     *              {@code
     *                  taskService.setAssignee(task.getId(),"安琪拉");
     *              }
     *          </li>
     *      </ol>
     *  </p>
     **/
    @Test
    public void testAssignee() {

    }

    /**
     *  <p>
     *      一个用户任务不能预先确定指派给哪个办理人，可能是对该任务进行操作的一批人，即候选人。候选组的概念与候选人比较类似，它不是把任务分配给一个或多个候选人，而是分配给用户组。
     *  Flowable支持将任务分配给一个或多个候选人或候选组。候选人或候选组中的用户能同时看到被分配的任务，需要其中一人认领该任务后才能办理。任务被某个候选人认领之后，该候选人将变成
     *  该任务的办理人，其他用户将不能再查看和办理该任务。Flowable将用户任务分配给候选人（组）有以下两种方式：
     *  <ul>
     *      <li>通过potentialOwner元素定义；</li>
     *      <li>通过candidateUsers/candidateGroups属性定义。</li>
     *  </ul>
     *  </p>
     *  <h3>通过potentialOwner元素定义</h3>
     *  <p>
     *      这是BPMN 2.0规定的标准方式。potentialOwner元素与humanPerformer一样，也是UserTask的子元素，其用法也很相似：
     *   <pre>
     *       {@code
     *          <userTask id='userTask1' name='分配给候选人（组）的用户任务' >
     *              <potentialOwner>
     *                  <resourceAssignmentExpression>
     *                      <formalExpression>
     *                          user(user1),user(user2),group(manager),group(staff)
     *                      </formalExpression>
     *                  </resourceAssignmentExpression>
     *              </potentialOwner>
     *          </userTask>
     *       }
     *   </pre>
     *   上述配置中，将用户任务分配给候选人 user1、user2，以及候选组manager、staff。注意，采用这种方式，需要指定表达式中的每个元素是用户还是群组，如果没有指定，工作流引擎会默认当作群组处理。<br/>
     *  </p>
     *  <h3>通过candidateUsers/candidateGroups属性定义</h3>
     *  <p>
     *      Flowable提供了扩展属性candidateUsers，用于把用户任务分配给候选人。其定义如下：
     *      <pre>
     *          {@code
     *              <userTask id="userTask1" name="分配给候选人的用户任务" flowable:candidateUsers="user1,user2" />
     *          }
     *      </pre>
     *      Flowable提供了扩展属性candidateGroups，用于把用户任务分配给候选组。其定义如下：
     *      <pre>
     *          {@code
     *              <userTask id="userTask2" name="分配给候选组的用户任务" flowable:candidateGroups="manager,staff" />
     *          }
     *      </pre>
     *  </p>
     **/
    @Test
    public void testCandidate() {

    }

    /**
     *  <h2>使用UEL表达式实现动态分配任务</h2>>
     *  <hr/>
     *  <p>
     *      Flowable使用UEL进行表达式解析，可在Java服务任务、执行监听器、任务监听器和条件顺序流等需要执行表达式的场景下使用。UEL表达式分为值表达式（UEL-value）和方法表达式（UEL-method）两种类型，
     *  Flowable对两者均提供了支持。
     *  </p>
     *  <h3>值表达式</h3>
     *  <p>
     *      值表达式解析结果为一个值。默认情况下，所有流程变量都可以在表达式中使用。此外，如果Flowable集成了Spring环境，则所有Spring Bean都可以在表达式中使用，例如：
     *      <pre>
     *          {@code
     *              //使用流程变量
     *              ${userName}
     *
     *              //使用Spring Bean的属性
     *              ${userBean.userName}
     *          }
     *      </pre>
     *  </p>
     *  <h3>方法表达式</h3>
     *  <p>
     *      方法表达式可以调用不带或带有参数的方法。调用不带参数的方法时，需确保在方法名称后添加空括号（用于区分表达式与值表达式）；调用带有参数的方法时，传递的参数可以是文字值或自行解析的表达式。例如：
     *      <pre>
     *          {@code
     *              //调用不带参数的方法
     *              ${taskAssigneeBean.getMangerOfProcessInitiator()}
     *
     *              //调用带有参数的方法
     *              ${taskAssigneeBean.getMangerOfDeptId("EP", execution)}
     *          }
     *      </pre>
     *      不论是值表达式，还是方法表达式，均支持原始类型（primitive）、Java Bean、列表（list）、数组（array）和映射（map）。这些表达式可以使用所有流程变量，同时Flowable还允许使用一些默认对象。
     *      <ul>
     *          <li>
     *              <b>execution：</b>当前正在运行的执行实例的DelegateExecution对象。
     *          </li>
     *          <li>
     *              <b>task：</b>当前正在操作的任务实例的DelegateTask对象，仅在任务监听器求值表达式中有效。
     *          </li>
     *          <li>
     *              <b>authenticatedUserId：</b>当前已认证的用户的唯一标识，如果没有用户通过身份验证，则该变量不可用。
     *          </li>
     *      </ul>
     *  </p>
     *  <h2>案例演示：</h2>
     *  <p>
     *      <b>流程定义文件：</b>{@link flow/assigneeTaskByUelProcess.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://cdn4.winhlb.com/2025/05/12/68219f4b62940.png" />
     *  </p>
     **/
    @Test
    public void testDynamic() {
        /**
         *  流程解释说明：
         *  1、用户任务userTask1使用flowable:assignee属性指定一个方法表达式，调用taskAssigneeBean的getMangerOfProcessInitiator()方法获取任务办理人；
         *
         *  2、用户任务userTask2使用flowable:candidateUsers属性指定一个方法表达式，调用taskAssigneeBean的getCandidateUsers()方法获取任务候选人；
         *
         *  3、用户任务userTask3使用flowable:candidateGroups属性指定一个方法表达式，调用taskAssigneeBean的getCandidateGroups()方法获取任务候选组；
         *
         *  4、用户任务userTask4使用flowable:assignee属性指定一个值表达式，通过taskAssigneeBean的designatedUserName属性设置任务办理人。
         **/
        Deployment deploy = repositoryService.createDeployment()
                .name("任务动态分配案例")
                .addClasspathResource("flow/assigneeTaskByUelProcess.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());

        //初始化taskAssigneeBean
        TaskAssigneeBean taskAssigneeBean = new TaskAssigneeBean();
        //设置designatedUserName属性值
        taskAssigneeBean.setDesignatedUserName("安琪拉");

        //设置流程变量
        Map<String, Object> varMap = MapUtil.builder(new HashMap<String, Object>())
        .put("taskAssigneeBean", taskAssigneeBean)
                .build();
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId(), varMap);
        TaskQuery taskQuery = taskService.createTaskQuery()
                .processInstanceId(procInst.getId());

        //查询第一个任务
        Task firstTask = taskQuery.singleResult();
        log.info("当前任务名称为：{}，办理人为：{}", firstTask.getName(), firstTask.getAssignee());
        //完成第一个任务
        taskService.complete(firstTask.getId());
        //查询第二个任务
        Task secondTask = taskQuery.singleResult();
        //查询任务的候选人信息
        List<IdentityLink> identityLinkList1 = taskService
                .getIdentityLinksForTask(secondTask.getId());
        log.info("当前任务名称为：{}，候选人为：{}", secondTask.getName(), JSON.toJSONString
                (identityLinkList1, identityLinkFilter));
        //候选人 user1 认领第二个任务
        taskService.claim(secondTask.getId(), "user1");
        //完成第二个任务
        taskService.complete(secondTask.getId());
        //查询第三个任务
        Task thirdTask = taskQuery.singleResult();
        //查询任务的候选组信息
        List<IdentityLink> identityLinkList2 = taskService
                .getIdentityLinksForTask(thirdTask.getId());
        log.info("当前任务名称为：{}，候选组为：{}" ,thirdTask.getName(), JSON.toJSONString
                (identityLinkList2, identityLinkFilter));
        //候选人 user3 认领第三个任务（用户 user3 是用户组group1的成员）
        taskService.claim(thirdTask.getId(), "user3");
        //完成第三个任务
        taskService.complete(thirdTask.getId());
        //查询第四个任务
        Task fourthTask = taskQuery.singleResult();
        log.info("当前任务名称为：{}，办理人为：{}", fourthTask.getName(), fourthTask.getAssignee());
        //完成第四个任务
        taskService.complete(fourthTask.getId());

        /**
         *  演示结果：
         * 2025-05-12 15:22:31.158  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 部署流程定义：dd91390a-2f01-11f0-82f6-50ebf6ec8f6d
         * 2025-05-12 15:22:31.162  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 流程定义编号：assigneeTaskByUelProcess:1:ddcfefbd-2f01-11f0-82f6-50ebf6ec8f6d
         * 2025-05-12 15:22:31.288  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：流程发起者的上级审批，办理人为：baby
         * 2025-05-12 15:22:31.515  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：候选人审批，候选人为：[{"type":"candidate","userId":"user1"},{"type":"candidate","userId":"user2"}]
         * 2025-05-12 15:22:31.548  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：候选组审批，候选组为：[{"groupId":"group2","type":"candidate"},{"groupId":"group1","type":"candidate"}]
         * 2025-05-12 15:22:31.585  INFO 89544 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：指定人审批，办理人为：安琪拉
         **/
    }

    //序列化助手
    SimplePropertyPreFilter identityLinkFilter = new SimplePropertyPreFilter(IdentityLink.class,
            "type","userId","groupId");


    /**
     *  <h2>使用任务监听器实现动态分配任务</h2>
     *  <p>
     *      Flowable提供了任务监听器，允许在任务执行过程中执行特定的Java程序或者表达式。通过这个机制，可以在任务监听器中使用编码方式实现动态分配任务。
     *      <b>流程定义文件：</b>{@link flow/assigneeTaskByTaskListenerProcess.bpmn20.xml}<br/>
     *      <b>流程预览图：</b><br/>
     *      <img src="https://cdn4.winhlb.com/2025/05/12/6821a4047f788.png" />
     *  </p>
     **/
    @Test
    public void testTaskListenAssignee() {
        /**
         *  流程解释：
         *  任务监听器UserTaskListener实现了org.flowable.engine.delegate.TaskListener接口，并重写了notify()方法，核心逻辑是根据不同的用户任务使用DelegateTask对象进行任务分配：
         *  1、用户任务userTask1和用户任务userTask4使用setAssignee()方法设置办理人；
         *
         *  2、用户任务userTask2使用addCandidateUsers()方法批量设置候选人，单个设置候选人可使用addCandidateUser()方法；
         *
         *  3、用户任务userTask3使用addCandidateGroups()方法批量设置候选组，单个设置候选组可以使用addCandidateGroup()方法。
         **/
        Deployment deploy = repositoryService.createDeployment()
                .name("任务动态分配-监听器案例")
                .addClasspathResource("flow/assigneeTaskByTaskListenerProcess.bpmn20.xml")
                .deploy();
        log.info("部署流程定义：{}", deploy.getId());
        ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery()
                .deploymentId(deploy.getId())
                .singleResult();
        log.info("流程定义编号：{}", procDef.getId());

        //设置流程变量
        Map<String, Object> varMap = MapUtil.builder(new HashMap<String, Object>())
                .put("designatedUserName", "安琪拉")
                .build();
        //启动流程实例
        ProcessInstance procInst = runtimeService.startProcessInstanceById(procDef.getId(), varMap);
        TaskQuery taskQuery = taskService.createTaskQuery()
                .processInstanceId(procInst.getId());

        //查询并完成第一个任务
        Task firstTask = taskQuery.singleResult();
        log.info("当前任务名称为：{}，办理人为：{}", firstTask.getName(), firstTask.getAssignee());
        taskService.complete(firstTask.getId());
        //查询第二个任务
        Task secondTask = taskQuery.singleResult();
        //查询任务的候选人信息
        List<IdentityLink> identityLinkList1 = taskService
                .getIdentityLinksForTask(secondTask.getId());
        log.info("当前任务名称为：{}，候选人为：{}", secondTask.getName(), JSON.toJSONString
                (identityLinkList1, identityLinkFilter));
        //候选人 user1 认领第二个任务
        taskService.claim(secondTask.getId(), "user1");
        //完成第二个任务
        taskService.complete(secondTask.getId());
        //查询第三个任务
        Task thirdTask = taskQuery.singleResult();
        //查询任务的候选组信息
        List<IdentityLink> identityLinkList2 = taskService
                .getIdentityLinksForTask(thirdTask.getId());
        log.info("当前任务名称为：{}，候选组为：{}", thirdTask.getName(), JSON.toJSONString
                (identityLinkList2, identityLinkFilter));
        //候选人 user1 认领第三个任务（用户 user1 是用户组group1的成员）
        taskService.claim(thirdTask.getId(), "user1");
        //完成第三个任务
        taskService.complete(thirdTask.getId());
        //查询并完成第四个任务
        Task fourthTask = taskQuery.singleResult();
        log.info("当前任务名称为：{}，办理人为：{}", fourthTask.getName(), fourthTask.getAssignee());
        taskService.complete(fourthTask.getId());

        /**
         *  演示结果：
         *  2025-05-12 15:36:11.046  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 部署流程定义：c638221a-2f03-11f0-8ca2-50ebf6ec8f6d
         * 2025-05-12 15:36:11.050  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 流程定义编号：c681390d-2f03-11f0-8ca2-50ebf6ec8f6d
         * 2025-05-12 15:36:11.168  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：流程发起者的上级审批，办理人为：baby
         * 2025-05-12 15:36:11.402  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：候选人审批，候选人为：[{"type":"candidate","userId":"user2"},{"type":"candidate","userId":"user1"}]
         * 2025-05-12 15:36:11.439  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：候选组审批，候选组为：[{"groupId":"group2","type":"candidate"},{"groupId":"group1","type":"candidate"}]
         * 2025-05-12 15:36:11.476  INFO 83616 --- [           main] c.b.net.flowable.task.user.UserTaskTest  : 当前任务名称为：指定人审批，办理人为：安琪拉
         **/
    }
}
